package client;


import infrastructure.HasherService;
import infrastructure.KVMessageImp;
import infrastructure.LoggingManager;

import java.security.NoSuchAlgorithmException;
import java.util.HashSet;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.LinkedBlockingQueue;

import org.apache.log4j.Logger;

import app_kvClient.KVClient;
import app_kvEcs.ServerInfo;

import clientCommunication.ClientSocketListener;
import clientCommunication.CommunicationAsync;
import clientCommunication.CommunicationFactory;
import clientCommunication.ICommunication;
import clientCommunication.ClientSocketListener.SocketStatus;
import common.messages.KVMessage;
import common.messages.KVMessage.MessageType;
import common.messages.KVMessage.StatusType;

public class KVStore implements KVCommInterface , ClientSocketListener {

	private Logger logger = LoggingManager.CreateClientLogger(KVStore.class);
	private String ipaddress;
	int port;
	private ICommunication communication;
	private Queue<KVMessage> messageQueue;
	private HasherService hasherService;
	private Set<KVStorageListener> listeners;
	Map<String, String> metadata;
	KVMessageImp lastMessageSend;
	/**
	 * Initialize KVStore with address and port of KVServer
	 * @param address the address of the KVServer
	 * @param port the port of the KVServer
	 */
	public KVStore(String ip, int port) {
		listeners = new HashSet<KVStorageListener>();
		this.ipaddress = ip;
		this.port = port;		
		messageQueue = new LinkedBlockingQueue<KVMessage>(10);
		try {
			this.hasherService = new HasherService();
		} catch (NoSuchAlgorithmException e) {
			logger.error("Error creating hasher service ", e);
		}
	}
		
	@Override
	public void connect() throws Exception {	
		logger.info("Connecting ...");
		//this.communication = CommunicationFactory.createCommunication(this.port + "", this.ipaddress, false);
		//this.communication.addListener(this);
		//Thread t = new Thread(communication);
		//t.start();
		this.communication = CommunicationFactory.createCommunication(this.port + "", this.ipaddress, true);
		//this.communication.addListener(this);
		this.communication.run();
		
		this.notifyMessage(communication.receiveMessage());
	}

	@Override
	public void disconnect() {
		logger.info("Disconnecting ...");
		if (communication != null){
			communication.disconnect();
		}
		
		communication = null;
		handleStatus(SocketStatus.DISCONNECTED);
	}

	@Override
	public synchronized KVMessage put(String key, String value) throws Exception {
		if (communication == null)
		{
			throw new Exception("Communication is not stablished");
		}
		
		KVMessage message = this.sendMessage(new KVMessageImp(MessageType.KV, value, key, StatusType.PUT));
		//wait();
		//KVMessage message =  messageQueue.remove();
		return message;
	}

	@Override
	public synchronized KVMessage get(String key) throws Exception {
		if (communication == null)
		{
			throw new Exception("Communication is not stablished");
		}
		
		KVMessage message = this.sendMessage(new KVMessageImp(MessageType.KV, null, key, StatusType.GET));
		//wait();
		//KVMessage message = messageQueue.remove();
		return message;
	}

	String getCurrentServerInfo(){
		return new ServerInfo(this.ipaddress, port).toString();
	}
	
	private KVMessage sendMessage(KVMessageImp message) throws Exception{		
		if (this.metadata != null) {
		
			String newServerInfo = this.hasherService.getServerInfo(this.metadata, message.getKey());
			if (newServerInfo == null){
				throw new Exception("There is no server associated to the key " + message.getKey());
			}
			
			String myServerInfo = this.getCurrentServerInfo();
			logger.info("Comparing ours " + myServerInfo + " vs " + newServerInfo);
			
			if (!newServerInfo.equals(myServerInfo)) {
				logger.info("Swtichgin comunication in client");
				this.switchCommunication(newServerInfo);
			}
		}
		
		logger.info("Sending " + message);
		return this.send(message);
	}
		
	private void switchCommunication(String serverInfo) throws Exception {
		String[] tokens = serverInfo.split(":");
		disconnect();		
		this.ipaddress = tokens[0];
		this.port = Integer.valueOf(tokens[1]);		
		connect();		
	}

	private KVMessage send(KVMessageImp message) throws Exception{
		this.lastMessageSend = message;
		this.communication.sendMessage(this.lastMessageSend);
		return receive();
	}
	
	private KVMessage receive() throws Exception{
		logger.info("Handling Message ");
		KVMessage kvMessage = communication.receiveMessage();
		if (kvMessage == null) {
			logger.info("MEssage received is null");
			return null;
		}
			
		logger.info("New message received " + kvMessage);
		if (kvMessage.getMessageType().equals(MessageType.INFO)) {			
			this.notifyMessage(kvMessage);
		} else if (kvMessage.getMessageType().equals(MessageType.KV)){
			//messageQueue.add(kvMessage);
			//notify();
			this.notifyMessage(kvMessage);
		} else if (kvMessage.getMessageType().equals(MessageType.METADATA)){
			String metadataValue = kvMessage.getValue();
			String metadataKey = kvMessage.getKey();
			
			// Process metadata
			logger.info("Getting new metadata " + metadataKey + "  " + metadataValue);
			this.metadata = hasherService.getHashing(metadataKey, metadataValue);
			
			// Resend message.
			kvMessage = this.sendMessage(this.lastMessageSend);
		}		
		
		return kvMessage;
	}
	
	private void notifyMessage(KVMessage kvMessage) {
		for(KVStorageListener listener : this.listeners) {
			listener.handleReply("Reply from server: " + kvMessage.getValue());
		}
	}

	@Override
	public void AddListener(KVStorageListener listener) {
		this.listeners.add(listener);
	}

		
	@Override
	public synchronized void handleNewMessage(KVMessage kvMessage) throws Exception {		
		
	}

	@Override
	public void handleStatus(SocketStatus status) {
		logger.info("New status received " + status);
		// Just notify the client in  a simple way
		for(KVStorageListener listener : this.listeners) {
			listener.handleReply("Server is: " + status.toString());
		}
	}
}
